Skip to content

PR 1: wire cua-cli non-interactive paths onto CuaAgentHarness - #22

Merged
rgarcia merged 2 commits into
mainfrom
hypeship/cua-cli-harness-pr1
Jun 12, 2026
Merged

PR 1: wire cua-cli non-interactive paths onto CuaAgentHarness#22
rgarcia merged 2 commits into
mainfrom
hypeship/cua-cli-harness-pr1

Conversation

@rgarcia

@rgarcia rgarcia commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 1 of 4 in the cua-cliCuaAgentHarness migration plan (see
docs/cua-cli-harness-migration.md).
Routes the non-interactive surface of cua-cli onto CuaAgentHarness +
pi 0.79 while leaving the interactive TUI on the legacy stack. Per the
plan, old and new dep trees coexist temporarily.

What changed

Engine (new wiring)

  • src/harness.ts — assembly. Builds a CuaAgentHarness from a Kernel
    client + browser + Session + skills + extraTools. Uses
    NodeExecutionEnv, createCodingTools(cwd) as extraTools, and a
    systemPrompt callback that composes
    resolveCuaRuntimeSpec(model).defaultSystemPrompt with
    formatSkillsForSystemPrompt(skills). Env-var API-key resolution
    through cua-ai's getCuaEnvApiKey.
  • src/harness-browser.ts — provisions Kernel browsers via the SDK
    directly (client.browsers.create/retrieve/deleteByID,
    captureScreenshot). Drops @onkernel/cua-translator's
    browserSession wrapper on new paths.
  • src/harness-sessions.ts — wraps JsonlSessionRepo for
    list / find-latest / resolve-by-ref. Tolerant of unknown files in
    the sessions root.
  • src/harness-models.ts — resolves -m through @onkernel/cua-ai's
    catalog. Accepts provider:model refs and bare ids that match
    exactly one catalog entry; default openai:gpt-5.5.
  • src/harness-skills.ts — pi 0.79 loadSkills over
    ~/.agents/skills/, <cwd>/.agents/skills/, and --skill paths.
  • src/harness-named-sessions.ts — named-session CLI re-implemented
    on the SDK. Metadata file format and path
    ($XDG_DATA_HOME/cua/named-sessions/<name>.json) preserved per the
    plan.

CLI surface

  • src/print.ts and src/action/harness-runner.ts drive the harness
    for --print and one-shot action subcommands
    (open/click/type/press/observe/url/screenshot/do).
    Action runners count turn_end events and abort at maxTurns via
    harness.abort. screenshot stays a direct SDK call.
  • src/output/harness-jsonl.ts sources the documented event schema
    (session_created, browser_created, tool_call, tool_result,
    turn_done, assistant_text_done, run_complete, error, opt-in
    deltas + images) from harness.subscribe. Adds a schema_version
    field on session_created.
  • src/cli-harness.ts orchestrates the new paths
    (runPrintCommand, runActionCommand, runModelsSubcommand,
    runSessionSubcommand).
  • src/cli.ts dispatches cua models, --print, action subcommands,
    and cua session ... to the new wiring. Interactive entry point
    continues on the legacy stack (intentional, per PR 1 scope).
  • Adds --thinking <level> flag (default low).

Tests + CI

  • vitest.config.ts and test/ directory with:
    • fixtures/scripted-provider.tsregisterApiProvider-based
      declarative step DSL (text deltas / canonical CUA tool calls /
      errors).
    • fixtures/fake-kernel.ts — plain object stubbing
      browsers.computer.{batch,captureScreenshot,getMousePosition,readClipboard},
      browsers.create/retrieve/deleteByID, and profiles.{retrieve,create}.
    • fixtures/harness.ts — assembles a real CuaAgentHarness through
      harness.ts plus the scripted provider and fake Kernel.
  • Test suites: --print text + jsonl envelope, action exit codes
    (ok / not_found / screenshot), session resolution
    (list / latest / prefix / legacy-tolerance / ambiguity), model-ref
    parsing (default / ref / bare-id / unknown / gemini alias), and
    harness-assembly invariants (createCodingTools assignable to
    extraTools, composed system prompt visible via
    before_agent_start, first-prompt screenshot delivered through
    harness.prompt({ images })).
  • .github/workflows/ci.yml gains a cli-unit job that runs the new
    vitest suite on every PR.

Open questions called out in the plan

  1. pi-coding-agent 0.79 createCodingTools is assignable to harness
    extraTools.
    Confirmed: both use AgentTool from
    @earendil-works/pi-agent-core. Covered by the
    installs createCodingTools as extraTools by default test in
    test/harness-assembly.test.ts.
  2. First-prompt screenshot via harness.prompt(text, { images }).
    Confirmed: the harness writes the user message with the image
    content to the session on first turn. Covered by the
    delivers the first prompt with an image attached test.
  3. Legacy session files do not break JsonlSessionRepo.list.
    Confirmed: list() skips unknown files and orphan directories in
    the sessions root. Covered by the
    tolerates legacy / unknown files in the sessions root test in
    test/harness-sessions.test.ts. Per-cwd sessions remain
    discoverable; no v2/ subdirectory is needed.

Behavioral contracts preserved

  • Action exit codes: 0 ok, 1 not_found, 2 error / usage.
  • Compact action stdout via formatCompact; errors to stderr.
  • --print -o jsonl event schema unchanged (only field sourcing
    changes); schema_version added.
  • Named-session metadata file format + path unchanged; session start | stop | list | show output preserved.
  • Session flags -c / -r / --session / --session-dir
    / --no-session and named-session -s <name> continue to drive
    session resolution.
  • --skill / -ns / --no-skills honored on the new paths.

Test plan

  • npm run typecheck
  • npm test --workspace @onkernel/cua-cli (18 tests pass)
  • npm test --workspace @onkernel/cua-agent -- --exclude "**/*.live.test.ts" (28 tests pass; no regressions)
  • npm test --workspace @onkernel/cua-ai (88 tests pass)
  • Manual cua --print smoke test against a live Kernel browser
    (deferred; CI exercises the same wiring against the scripted
    provider)

Deviations from the plan

None. Interactive mode stays on the old stack as called out in the PR 1
section; PR 2 rebuilds it on pi-tui 0.79.

Out of scope (PR 2-4)

  • Interactive TUI on harness + pi-tui 0.79 (PR 2).
  • Deleting agent.ts, old models.ts, config.ts, old sessions.ts,
    skills.ts, agent-prompt.ts, and dropping the @mariozechner/* +
    cua-translator + cua-<provider> + smol-toml deps (PR 3).
  • Removing packages/cua-{translator,openai,anthropic,gemini,tzafon,yutori}
    from the workspace (PR 4).

Note

Medium Risk
Medium risk from a large CLI execution-path swap (auth, sessions, Kernel browser lifecycle) while dual stacks coexist; mitigated by preserved contracts and new unit tests.

Overview
This PR moves non-interactive cua (--print, action subcommands, models, session) onto CuaAgentHarness with @onkernel/cua-agent, @onkernel/cua-ai, and the Kernel SDK—replacing the legacy agent/translator stack on those paths. The interactive TUI still uses the old wiring.

New harness modules handle browser provisioning, JSONL sessions, model refs (provider:model), skills, named sessions, print/jsonl output, and action runs (including turn caps via harness.abort and SDK-only screenshot). --thinking is added; jsonl gains schema_version on session_created and provider-qualified model refs.

Vitest replaces node:test for CLI unit tests (scripted provider + fake Kernel fixtures), and CI adds a cli-unit job. Legacy @mariozechner/* deps remain for the TUI during the migration.

Reviewed by Cursor Bugbot for commit c4d78e9. Bugbot is set up for automated code reviews on this repo. Configure here.

PR 1 of the cua-cli → CuaAgentHarness migration plan
(docs/cua-cli-harness-migration.md). Wires the non-interactive
surface of cua-cli onto CuaAgentHarness + pi 0.79 while leaving the
interactive TUI on the legacy stack for now.

Engine:
- harness.ts assembles a CuaAgentHarness from a Kernel client + browser
  + jsonl Session + cua-cli skills + pi-coding-agent's createCodingTools
  as extraTools.
- harness-browser.ts provisions Kernel browsers via the SDK directly
  (drops the cua-translator browserSession wrapper on new paths).
- harness-sessions.ts wraps JsonlSessionRepo for list / find-latest /
  resolve-by-ref and is tolerant of unknown files in the sessions root.
- harness-models.ts resolves -m flags through @onkernel/cua-ai's
  listCuaModels / parseCuaModelRef catalog; default is openai:gpt-5.5.
- harness-skills.ts loads skills via pi 0.79 loadSkills.
- harness-named-sessions.ts re-implements the named-session CLI on the
  SDK.

CLI:
- print.ts and action/harness-runner.ts drive the harness for --print
  and one-shot action subcommands (open/click/type/press/observe/url/
  screenshot/do), aborting at maxTurns via harness.abort.
- output/harness-jsonl.ts sources the documented event schema from
  harness.subscribe and stamps a schema_version field.
- cli.ts dispatches `cua models`, `--print`, action subcommands, and
  `cua session ...` to the new wiring through cli-harness.ts. The
  interactive entry point still uses the legacy stack.

Tests + CI:
- vitest config + fixtures: registerApiProvider-based scripted-provider
  driver and a fake Kernel client that stubs browsers.computer.batch /
  captureScreenshot. Test suites cover --print text + jsonl envelope,
  action exit codes (ok / not_found / error / screenshot), session
  resolution (list, latest, prefix, legacy-tolerance), model-ref
  parsing (default, ref pass-through, bare-id, ambiguity), and harness
  assembly invariants (coding-tools assignable to extraTools, default
  system prompt + skill block composition, first-prompt screenshot via
  harness.prompt({ images })).
- CI gains a cli-unit job that runs the new tests on every PR.

Old and new dep trees coexist intentionally in this PR; @mariozechner/*
stays on the package while interactive remains on it. PR 2 rebuilds
the TUI on harness + pi-tui 0.79 and PR 3 deletes the legacy code.

@rgarcia rgarcia left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against docs/cua-cli-harness-migration.md (PR 1 section). Ran the build and all three test suites locally: typecheck green, cua-cli 18/18, cua-agent 28/28, cua-ai 88/88. Confirmed the node dist/cli.js ERR_MODULE_NOT_FOUND reproduces with main's sources (pre-existing, fine to defer). No edits under packages/agent or packages/ai; scope is clean; the three open questions are answered and test-backed.

Overall the architecture matches the plan well (harness assembly, SDK provisioning, jsonl repo, pi skills, env auth, scripted-provider fixture through the real buildCuaHarness). But a handful of behavioral contracts the plan calls out as "preserved" are not, so this needs another pass.

Major

  1. One-shot action subcommands now persist a session file per invocation. runActionCommandsetupHarnessRuntimeresolveSession falls through to createSession (cli-harness.ts:305) whenever --no-session isn't passed. Legacy runActionSub skipped the SessionManager entirely for actions without -s, and the top-level README documents exactly that ("One-shot action subcommands (without -s) also skip the transcript"). Besides the disk clutter, every cua click ... now changes what -c/--session latest resolves to. Suggested fix: in the action path, only resolve a persistent session when flags.namedSession (or an explicit session flag) is set; use the in-memory session otherwise.

  2. Action runner drops the first-prompt screenshot. Legacy actions went through promptWithScreenshot, attaching a screenshot to the first user message of a fresh transcript. The new runAction calls opts.harness.prompt(prompt) with no images (action/harness-runner.ts:102), and only yutori injects a screenshot at payload time. So cua click/type/observe/url/do run the first turn blind for openai/anthropic/google/tzafon — observe's prompt even forbids taking actions, so the model can't recover by calling the screenshot tool without violating its instructions. print.ts got this right (maybeInitialScreenshot); the action path needs the same treatment, which also matches the plan's agent-prompt.ts row ("first prompt of a fresh session attaches a screenshot ... harness.prompt(text, { images })").

  3. cua session start --profile <name> regressed. runSessionSubcommand passes the raw --profile selector as profileId (cli-harness.ts:532) and startNamedSession puts it straight into profile: { id: ... } (harness-named-sessions.ts:127-129). Legacy resolved name-or-id (and auto-created missing names) via profileSelector. The new harness-browser.ts already has resolveProfileId with exactly the legacy semantics — startNamedSession should use it. As written, a profile name is sent as an id, and the raw selector is persisted into meta.profile_id (legacy stored the resolved id).

  4. --session <path> and named-session transcript resolution are exact-string, cwd-filtered matches. resolveSessionRef (harness-sessions.ts:68) and the named-session continuation lookup (cli-harness.ts:300) compare the input against metadata.path from repo.list({ cwd }). Consequences: relative paths fail; an absolute path to a session created from another cwd fails; and cua -s <name> --print run from a different directory than where the transcript was created silently starts a fresh session and clobbers transcript_path. Legacy attached to any given path regardless of cwd. Suggested fix: path.resolve the input and match against repo.list() without the cwd filter (or load metadata directly from the file) for the path/transcript-path cases. The contract list explicitly includes --session <path|prefix|latest>.

Minor

  1. jsonl schema drift beyond the documented change. browser_created.profile_id is now always omitted (profile_id: undefined, output/harness-jsonl.ts) — legacy emitted the resolved profile id when --profile was used; CuaBrowserHandle just doesn't carry it. Also session_created.model changed value format from bare id (gpt-5.5) to ref (openai:gpt-5.5). Both may be acceptable under schema_version=1, but the plan says "Note schema version in the README" and neither README nor the top-level "Session transcripts"/output docs were touched.

  2. Provider *_BASE_URL env overrides aren't wired on the new paths. The plan's config.ts row calls for "<PROVIDER>_BASE_URL env overrides spread onto the model object (a few lines)", and cua --help still documents them. Only KERNEL_BASE_URL is honored. (Legacy applied at least YUTORI_BASE_URL via applyProviderBaseUrl.)

  3. --thinking silently coerces unknown values to low (mapThinkingLevel default case). A typo like --thinking hgih should be a usage error (exit 2) per the exit-code contract, not silently low.

  4. runAction lost two legacy fallbacks: (a) when no text_delta events arrive, legacy pulled the final assistant message text from state before parsing — the new runner only accumulates deltas even though harness.prompt() returns the final AssistantMessage; (b) tool-error extraction no longer prefers details.error over content text.

  5. -c/latest semantics changed from last-modified to last-created. Legacy sorted by file mtime; findLatestSession sorts by createdAt (and repo.list already returns createdAt-desc, so the extra sort is redundant). After resuming an older session, -c now continues a different session than legacy would.

  6. Test coverage gaps vs the acceptance list: no test for exit code 2 (error path), none for the maxTurns turn-cap abort, and the jsonl test has no tool steps so tool_call/tool_result shapes are unasserted. All cheap to add on the existing fixtures.

Nit

  • HarnessRunOptions.session (and verbose) are required but unused in runAction; cli-harness even builds a throwaway fallbackInMemorySession() to satisfy it, duplicating the in-memory session already created in setupHarnessRuntime. Let HarnessRuntime carry the session it actually built the harness with, and drop the dead params.
  • await import("@onkernel/cua-agent") for InMemorySessionRepo in two places in cli-harness.ts — the module is already statically imported at the top of the file.
  • resolveAuth could use cua-ai's requireCuaEnvApiKey, which names the env vars to set, instead of the generic missing API key for provider "x".
  • buildCuaHarness's systemPrompt callback closes over the build-time skills array; the callback receives resources — using resources.skills matches the plan wording and will matter once PR 2 calls setResources.
  • Transitional interop worth a PR-body note: -s <name> transcript_path is shared between the legacy interactive stack and the new repo format; a --print/action run rewrites it to a v2 path that the legacy interactive stack will then try to open.
  • vitest.config.ts sets server.host — a Vite dev-server option that does nothing under vitest --run.

Items 1-4 are should-fix-before-merge; the rest are take-or-leave. The structure is right and PR 2/3 should slot in cleanly once the action-path contracts are restored.

- Action subcommands no longer create on-disk session files unless an
  explicit session flag is set (-s / -c / -r / --session).
- Action runner reattaches the legacy first-prompt screenshot via
  harness.prompt({ images }) on fresh sessions.
- runAction falls back to the returned AssistantMessage text when no
  text_delta events arrived, and prefers details.error over content
  text when extracting tool errors.
- cua session start --profile <name> now goes through resolveProfileId
  so a name is created/looked up before provisioning, and the resolved
  id is what gets persisted in named-session metadata.
- --session <path> and named transcript_path resolution accept paths
  from any cwd by reading the session header directly.
- -c / latest sorts by file mtime (legacy semantics) instead of header
  createdAt.
- jsonl browser_created emits profile_id when --profile is used, and
  the README documents the schema_version + model-ref change.
- <PROVIDER>_BASE_URL env overrides flow through buildCuaHarness onto
  the resolved model object.
- --thinking values are validated up front; unknown values exit 2.
- HarnessRuntime exposes the assembled Session, dropping the
  fallback-in-memory session and dynamic InMemorySessionRepo imports.
- systemPrompt callback reads resources.skills, so a future
  setResources() call picks up the new skill set.
- resolveAuth uses requireCuaEnvApiKey for an env-var-named error.
- Tests cover error exit 2, the turn-cap abort path, jsonl tool steps,
  the first-prompt screenshot, and --session <path> from a different
  cwd.
@rgarcia
rgarcia marked this pull request as ready for review June 12, 2026 04:03
@firetiger-agent

Copy link
Copy Markdown

Firetiger deploy monitoring skipped

This PR didn't match the auto-monitor filter configured on your GitHub connection:

PRs in the kernel, infra, hypeman, and hypeship repos. kernel is a ~mono repo with many logical services underneath, ensure to focus on the implicated service for the PR

Reason: This PR is in the cua-cli repository, which is not among the operator's monitored repos (kernel, infra, hypeman, hypeship).

To monitor this PR anyway, reply with @firetiger monitor this.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Skill print skips screenshot
    • runPrint now captures first-turn images before dispatch and, when needed for /skill: prompts, sends a skill-formatted prompt with those images so fresh skill runs retain screenshot context.
  • ✅ Fixed: Unknown skill slash command mishandled
    • Unknown /skill:<name> inputs are now expanded into the legacy missing-skill explanatory prompt plus remainder text instead of forwarding the raw slash command.

Create PR

Or push these changes by commenting:

@cursor push 311cbe4cb1

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit c4d78e9. Configure here.

assistant = await opts.harness.skill(invocation.skill.name, invocation.remainder || undefined);
} else {
const images = await maybeInitialScreenshot(opts);
assistant = await opts.harness.prompt(opts.prompt, images ? { images } : undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skill print skips screenshot

Medium Severity

For --print, prompts that match /skill:&lt;name&gt; call harness.skill and never run maybeInitialScreenshot. On a new session (not resumed), the legacy path still attached a first-turn browser screenshot via promptWithScreenshot, which computer-use tasks often need.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c4d78e9. Configure here.

assistant = await opts.harness.skill(invocation.skill.name, invocation.remainder || undefined);
} else {
const images = await maybeInitialScreenshot(opts);
assistant = await opts.harness.prompt(opts.prompt, images ? { images } : undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unknown skill slash command mishandled

Medium Severity

When --print receives /skill:&lt;name&gt; but no loaded skill matches, the harness path sends the raw slash command to harness.prompt. The previous stack expanded that into a clear “skill not found” message plus the user’s remainder text.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c4d78e9. Configure here.

@rgarcia
rgarcia merged commit 1a95e2d into main Jun 12, 2026
7 checks passed
@rgarcia
rgarcia deleted the hypeship/cua-cli-harness-pr1 branch June 12, 2026 04:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant